iT邦幫忙

2022 iThome 鐵人賽

DAY 12
0
Modern Web

就是要搞懂 JavaScript 啦!系列 第 12 篇

Day12 提升:到底是在提什麼升什麼啦!

  • 分享至 

  • xImage
  •  

變數的提升

我們已經知道,變數依賴著作用域存活,也就是遵循著一系列規則被存取。

而在變數的宣告和賦值上,有一個微妙的細節。

先前已經提過,進行變數查詢時如果找不到識別字的參考,就會回報 ReferenceError:

console.log(a);
// ReferenceError: a is not defined

加上宣告與賦值後才能正常取得

let a;
a = 1;
console.log(a); // 1

甚至不賦值的話,JS 也會給予預設值 undefined

let a;
console.log(a); // undefined

但如果在宣告之前存取:

console.log(a);
// ReferenceError: Cannot access 'a' before initialization
let a;

可以看到同樣出現了 ReferenceError,但奇怪,這裡不是寫 a is not defined,而是出現 Cannot access 'a' before initialization 。

照理來說,在宣告之前使用,不是應該跟 a is not defined 一樣,找不到識別字對應的參考嗎?

為什麼顯示的錯誤卻表示 a 尚未初始化?這兩者到底有什麼不同?

解開這個答案之前,來看看更神奇的事情:

console.log(a); // undefined
var a;

在上面的程式碼中,程式打印了一個在此之前未宣告過的 a 變數,隨後才在下一行宣告它,結果程式......毫無錯誤地印出了 undefined !?

要解答這兩個謎題,就需要來探討,JS 實際上都做了些什麼。


解開祕密的鑰匙

所以說,為什麼以下的結果會是 undefined ?

console.log(a); // undefined
var a;

問題的解答,藏在 JS 的編譯時期,在程式執行之前就已經發生了某些事,因此等執行到 console.log(a) 的時候,才會出現上面那樣的情況。

同一段的程式碼,對人類來說會直覺式地從上往下讀,並對其賦予「時間」的概念,上面的程式碼先執行,下面的程式碼後執行。

這個概念對電腦基本上來說也是一樣的,只是每個階段處理的內容不同,所以最後的結果有時會在人腦的預期之外。

先前已經提過,JS 包含編譯這個過程,在這個時期,JS 編譯器就已經完成全部的「宣告」作業,並將識別字存入記憶體當中,同時處理好所有作用域存取規則。

因此在執行 console.log(a); 之前,var a; 早在編譯時期便處理完畢,並給予預設值 undefined ,因此來到 console.log(a); 時,就能毫無障礙地進行 RHS,然後順利把值傳給 console.log 執行。

以上整個過程所產生的結果,就像把 var a; 這行程式碼搬移到最前面一樣,因此這樣的現象被稱為「提升(Hoisting)」。

// var a; 早在編譯期處理完畢,像是被放到了最上面一行
console.log(a); // undefined
var a; // 被提升到第一行了

與此同時,由於變數的存取遵守作用域規則,因此所有宣告「看起來」都被提升到了所屬作用域的最前面,畢竟再上去,也就是更外層的作用域並無法存取這個變數。

// 這裡無法取得 a
function foo(){
  // var a; 被提升到此處,是 a 最早能夠被取得的地方
  console.log(a) // undefined
  var a;
}
foo();

但賦值的部分則是在執行時期處理,並沒有提升的行為:

// var a 宣告被提升到最頂端
function foo() {
  // var b 被提升到 foo 作用域的最頂端
  console.log(b); // undefined
  b = "B"; // 於執行時期才處理,留在原地
  var b; // 於編譯時期就處理完畢,因此已經被提升到上面了
  console.log(b); // B
  
  // foo 執行的時候,a 完成宣告但尚未賦值
  console.log("a in foo:", a); // a in foo: undefined
}

foo();
var a = "A"; // 直到這裡才賦值給 a
console.log("a in global:", a); // a in global: A

總結來說,由於 JS 會在編譯時期處理所有宣告,結果就是程式內部的宣告「看上去」被往前移到了所屬作用域的最頂端,這就是所謂的「提升(Hoisting)」。


JS 的宣告補丁

理解提升的原理後,回頭來看看 JS 在 ES6 新增的宣告補丁,也就是 let 和 const 這兩個關鍵字。

前面已經說過,let 和 const 兩者唯一的差別,在於 const 僅能在宣告時被賦值,而 let 則能夠重複賦值無數次。

並且 let 和 const 能夠創造區塊作用域(block scope),但 var 不能。除此之外,它們和 var 在提升時的表現其實也有所不同。

讓我們回到這段程式碼:

console.log(a);
let a;

前面已經提過,程式在這裡會回報 ReferenceError 。但並不是顯示 a is not defined,而是 Cannot access 'a' before initialization,而這就是 JS 對 var 和 let 做出的區別。

由 let 宣告的變數同樣在編譯時期就處理完變數宣告,所以在遇到 console.log(a) 之前,記憶體當中就存在這個變數了。

但是 JS 處理 var 宣告的變數時,會同時進行初始化並賦予預設值 undefined,let 和 const 在編譯時期卻沒有這個步驟。

一直要等到程式開始執行,並跑到進行宣告的那行程式碼,才會對 let 和 const 宣告的變數進行初始化。

如果在這之前存取這個變數,就會出現 Cannot access 'a' before initialization 這樣的錯誤訊息。

JS 對此有個專有名詞,叫做「暫時性死區(TDZ, Temporal Dead Zone)」。也就是 let 或 const 被提升後,尚未進行初始化、還無法存取的時期。

function foo() {
   // a 被提升到這裡,但處於 TDZ 狀態中
    bar(); // 在 a 的 TDZ 期間執行
    let a = "Hello"; // 遇到 a 的宣告,TDZ 結束
    function bar(){
      console.log(a)
    }
}
foo()
// ReferenceError: Cannot access 'a' before initialization

在上面的程式碼中,調用 bar() 時,記憶體中只是保留了 a 的位置,卻尚未初始化並給予預設值,因此在執行 console.log(a) 時遇到錯誤:「無法取用尚未初始化的變數 a 」。

JS 之所以會在 ES6 新增 let 和 const 這兩個宣告補丁,也是由於「提升」這個特性,讓 var 宣告的變數能夠在進行宣告的該行程式碼之前讀取,這樣的特性容易引起困惑。

比方說,你在某個地方取用了變數 a,卻沒注意到你把程式碼寫在 var a = true; 的前面,按照 JS 的邏輯,此時的 a 已經被初始化,並擁有 undefined 這個預設值,所以程式完全能夠正常運行,執行結果卻和預想的相反:

if(a){
  console.log("Let's start!") // 永遠不會跑到這行來
}

var a = true;

但如果改成 let,至少程式會告訴你,似乎有哪裡不對勁:

if(a){
  console.log("Let's start!") 
}

let a = true;
// ReferenceError: Cannot access 'a' before initialization

總結

經過上面對變數提升的討論,這裡能夠得出一個結論:「在 JS 中,不論程式碼撰寫順序為何,宣告永遠早於賦值。」因為宣告的處理,早在程式編譯時期就已經完成了。

除此之外,JS 在 ES6 新增的 let 和 const 所擁有的特性,顯然能夠降低程式碼的錯誤率。因此這兩個關鍵字出現後,便有不少人提倡全面廢止 var,而僅僅使用 let 和 const 進行宣告。

那麼,關於 JS 的提升終於在本系列最長的一篇文章中結束......這怎麼可能呢!作為 JS 的魔王關卡之一,提升還有另一段故事要講。

在進入下一章之前先給你一段程式碼,猜猜看會印出什麼結果?

console.log(a);
var a;
function a(){
  console.log("JavaScript!")
}

如果一切如你所料,那這裡給你一個掌聲。若是你對答案感到困惑,就來下一篇文章找找解答吧!


參考資料


上一篇
Day11 咱們井水不犯河水──作用域的優點
下一篇
Day13 提升:為什麼要提升?留在原地不好嗎?
系列文
就是要搞懂 JavaScript 啦! 共 73 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言